package com.gitee.wsl.compose.viewmodel

import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ViewModel
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.lifecycle.viewModelScope
import com.gitee.wsl.api.Closeable
import com.gitee.wsl.data.input.event.EventHandler
import com.gitee.wsl.data.input.viewmodel.InputModel
import com.gitee.wsl.data.input.viewmodel.InputModelConfiguration
import com.gitee.wsl.data.input.viewmodel.InputModelImpl
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch

open class BallastInputViewModel<Inputs : Any, Events : Any, State : Any>
private constructor(
    private val impl: InputModelImpl<Inputs, Events, State>,
    providedCoroutineScope: CoroutineScope? = null
) : ViewModel(
    *listOfNotNull(
        providedCoroutineScope.asCloseable()
    ).toTypedArray()
), InputModel<Inputs, Events, State> by impl {

    /**
     * Construct a new AndroidViewModel instance, which runs the Ballast processor on [coroutineScope]. This
     * coroutineScope will be cancelled directly before {@link #onCleared()} is called.
     *
     * If [coroutineScope] also implements [Closeable], then the scope's own [Closeable.close] will be called when the
     * ViewModel is cleared. Otherwise, the scope will be wrapped in `Closeable { coroutineScope.cancel() }`.
     *
     * <p>
     * You should <strong>never</strong> manually construct a ViewModel outside of a
     * {@link ViewModelProvider.Factory}.
     */
    constructor(
        config: InputModelConfiguration<Inputs, Events, State>,
        coroutineScope: CoroutineScope,
    ) : this(
        impl = InputModelImpl("AndroidViewModel", config),
        providedCoroutineScope = coroutineScope
    )

    init {
        // if a coroutineScope was provided through the constructor, use that. Otherwise, fall-back to using viewModelScope
        impl.start(providedCoroutineScope ?: viewModelScope)
    }

    fun observeStatesOnLifecycle(
        lifecycleOwner: LifecycleOwner,
        targetState: Lifecycle.State = Lifecycle.State.RESUMED,
        onStateChanged: (State) -> Unit,
    ): Job = with(lifecycleOwner) {
        lifecycleScope.launch {
            lifecycle.repeatOnLifecycle(targetState) {
                observeStates()
                    .onEach(onStateChanged)
                    .launchIn(this)
            }
        }
    }

    fun attachEventHandlerOnLifecycle(
        lifecycleOwner: LifecycleOwner,
        handler: EventHandler<Inputs, Events, State>,
        targetState: Lifecycle.State = Lifecycle.State.RESUMED,
    ): Job = with(lifecycleOwner) {
        lifecycleScope.launch {
            lifecycle.repeatOnLifecycle(targetState) {
                impl.attachEventHandler(handler)
            }
        }
    }

    fun runOnLifecycle(
        lifecycleOwner: LifecycleOwner,
        eventHandler: EventHandler<Inputs, Events, State>,
        targetState: Lifecycle.State = Lifecycle.State.RESUMED,
        onStateChanged: (State) -> Unit,
    ): Job = with(lifecycleOwner) {
        lifecycleScope.launch {
            joinAll(
                observeStatesOnLifecycle(lifecycleOwner, targetState, onStateChanged),
                attachEventHandlerOnLifecycle(lifecycleOwner, eventHandler, targetState),
            )
        }
    }

    fun attachEventHandler(
        coroutineScope: CoroutineScope = impl.viewModelScope,
        handler: EventHandler<Inputs, Events, State>
    ): Job {
        return coroutineScope.launch {
            impl.attachEventHandler(handler)
        }
    }

    companion object {
        private fun CoroutineScope?.asCloseable(): Closeable? {
            if (this == null) return null
            if (this is Closeable) return this

            return object:Closeable {
                override fun close() {
                    this@asCloseable.cancel()
                }
            }
        }
    }
}
